msg_tool\scripts\softpal\scr/
mod.rs

1//! Softpal script (.src)
2mod disasm;
3
4use crate::ext::io::*;
5use crate::scripts::base::*;
6use crate::types::*;
7use crate::utils::encoding::*;
8use anyhow::Result;
9use disasm::*;
10use std::collections::HashMap;
11use std::io::{Read, Write};
12
13#[derive(Debug)]
14/// Softpal script builder
15pub struct SoftpalScriptBuilder {}
16
17impl SoftpalScriptBuilder {
18    /// Create a new Softpal script builder
19    pub fn new() -> Self {
20        Self {}
21    }
22}
23
24impl ScriptBuilder for SoftpalScriptBuilder {
25    fn default_encoding(&self) -> Encoding {
26        Encoding::Cp932
27    }
28
29    fn build_script(
30        &self,
31        buf: Vec<u8>,
32        filename: &str,
33        encoding: Encoding,
34        _archive_encoding: Encoding,
35        config: &ExtraConfig,
36        archive: Option<&Box<dyn Script>>,
37    ) -> Result<Box<dyn Script>> {
38        Ok(Box::new(SoftpalScript::new(
39            buf, filename, encoding, config, archive,
40        )?))
41    }
42
43    fn extensions(&self) -> &'static [&'static str] {
44        &["src"]
45    }
46
47    fn script_type(&self) -> &'static ScriptType {
48        &ScriptType::Softpal
49    }
50
51    fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
52        if buf_len >= 4 && buf.starts_with(b"Sv20") {
53            return Some(10);
54        }
55        None
56    }
57}
58
59#[derive(Debug)]
60/// Softpal SRC Script
61pub struct SoftpalScript {
62    data: MemReader,
63    strs: Vec<PalString>,
64    texts: MemReader,
65    encoding: Encoding,
66    label_offsets: Vec<u32>,
67    add_message_index: bool,
68}
69
70impl SoftpalScript {
71    /// Create a new Softpal script
72    pub fn new(
73        buf: Vec<u8>,
74        filename: &str,
75        encoding: Encoding,
76        config: &ExtraConfig,
77        archive: Option<&Box<dyn Script>>,
78    ) -> Result<Self> {
79        let texts = Self::load_texts_data(Self::load_file(filename, archive, "TEXT.DAT")?)?;
80        let points_data = MemReader::new(Self::load_file(filename, archive, "POINT.DAT")?);
81        let label_offsets = Self::load_point_data(points_data)?;
82        let strs = Disasm::new(&buf, &label_offsets)?.disassemble::<MemWriter>(None)?;
83        Ok(Self {
84            data: MemReader::new(buf),
85            strs,
86            encoding,
87            texts,
88            label_offsets,
89            add_message_index: config.softpal_add_message_index,
90        })
91    }
92
93    fn load_file(filename: &str, archive: Option<&Box<dyn Script>>, name: &str) -> Result<Vec<u8>> {
94        if let Some(archive) = archive {
95            Ok(archive
96                .open_file_by_name(name, true)
97                .map_err(|e| anyhow::anyhow!("Failed to open file {} in archive: {}", name, e))?
98                .data()?)
99        } else {
100            let mut path = std::path::PathBuf::from(filename);
101            path.set_file_name(name);
102            std::fs::read(path).map_err(|e| anyhow::anyhow!("Failed to read file {}: {}", name, e))
103        }
104    }
105
106    fn load_texts_data(data: Vec<u8>) -> Result<MemReader> {
107        let mut writer = MemWriter::from_vec(data);
108        if writer.data.len() >= 0x14 {
109            let ind = writer.cpeek_u32_at(0x10)?;
110            writer.pos = 0x10;
111            if ind != 0 {
112                let mut shift = 4;
113                for _ in 0..(writer.data.len() / 4 - 4) {
114                    let mut data = writer.cpeek_u32()?;
115                    let mut add = data.to_le_bytes();
116                    add[0] = add[0].rotate_left(shift);
117                    shift = (shift + 1) % 8;
118                    data = u32::from_le_bytes(add);
119                    data ^= 0x084DF873 ^ 0xFF987DEE;
120                    writer.write_u32(data)?;
121                }
122            }
123        }
124        Ok(MemReader::new(writer.into_inner()))
125    }
126
127    fn load_point_data(mut data: MemReader) -> Result<Vec<u32>> {
128        let mut magic = [0u8; 16];
129        data.read_exact(&mut magic)?;
130        if magic != *b"$POINT_LIST_****" {
131            return Err(anyhow::anyhow!("Invalid point list magic: {:?}", magic));
132        }
133        let mut label_offsets = Vec::new();
134        while !data.is_eof() {
135            label_offsets.push(data.read_u32()? + CODE_OFFSET);
136        }
137        label_offsets.reverse();
138        Ok(label_offsets)
139    }
140}
141
142impl Script for SoftpalScript {
143    fn default_output_script_type(&self) -> OutputScriptType {
144        OutputScriptType::Json
145    }
146
147    fn default_format_type(&self) -> FormatOptions {
148        FormatOptions::None
149    }
150
151    fn is_output_supported(&self, _: OutputScriptType) -> bool {
152        true
153    }
154
155    fn multiple_message_files(&self) -> bool {
156        true
157    }
158
159    fn extract_messages(&self) -> Result<Vec<Message>> {
160        let mut messages = Vec::new();
161        let mut name = None;
162        let max_len = self.texts.data.len() as u32;
163        for str in &self.strs {
164            let addr = self.data.cpeek_u32_at(str.offset as u64)?;
165            if addr - 4 > max_len {
166                continue;
167            }
168            let idx = self.texts.cpeek_u32_at(addr as u64)?;
169            let text = self.texts.cpeek_cstring_at(addr as u64 + 4)?;
170            let text =
171                decode_to_string(self.encoding, text.as_bytes(), false)?.replace("<br>", "\n");
172            let text = if self.add_message_index {
173                format!("[{}]{}", idx, text)
174            } else {
175                text
176            };
177            match str.typ {
178                StringType::Name => {
179                    if text.is_empty() {
180                        continue; // Skip empty names
181                    }
182                    name = Some(text);
183                }
184                StringType::Message => messages.push(Message {
185                    name: name.take(),
186                    message: text,
187                }),
188                StringType::Hover => messages.push(Message::new(text, None)),
189                StringType::Label => {} // Ignore labels
190            }
191        }
192        Ok(messages)
193    }
194
195    fn extract_multiple_messages(&self) -> Result<HashMap<String, Vec<Message>>> {
196        let mut hovers = Vec::new();
197        let mut messages = Vec::new();
198        let mut label = None;
199        let mut name = None;
200        let mut result = HashMap::new();
201        let max_len = self.texts.data.len() as u32;
202        for str in &self.strs {
203            let addr = self.data.cpeek_u32_at(str.offset as u64)?;
204            if addr - 4 > max_len {
205                continue;
206            }
207            let idx = self.texts.cpeek_u32_at(addr as u64)?;
208            let text = self.texts.cpeek_cstring_at(addr as u64 + 4)?;
209            let ptext =
210                decode_to_string(self.encoding, text.as_bytes(), false)?.replace("<br>", "\n");
211            let text = if self.add_message_index {
212                format!("[{}]{}", idx, ptext)
213            } else {
214                ptext.clone()
215            };
216            match str.typ {
217                StringType::Name => {
218                    if text.is_empty() {
219                        continue; // Skip empty names
220                    }
221                    name = Some(text);
222                }
223                StringType::Message => messages.push(Message::new(text, name.take())),
224                StringType::Hover => hovers.push(Message::new(text, None)),
225                StringType::Label => {
226                    if !messages.is_empty() {
227                        let key = label.take().unwrap_or_else(|| "default".to_string());
228                        if result.contains_key(&key) {
229                            eprintln!(
230                                "Warning: Duplicate label '{}', overwriting previous messages.",
231                                key
232                            );
233                            crate::COUNTER.inc_warning();
234                        }
235                        result.insert(key, messages);
236                        messages = Vec::new();
237                    }
238                    label = Some(ptext);
239                }
240            }
241        }
242        if !messages.is_empty() {
243            let key = label.take().unwrap_or_else(|| "default".to_string());
244            result.insert(key, messages);
245        }
246        if !hovers.is_empty() {
247            result.insert("hover".to_string(), hovers);
248        }
249        Ok(result)
250    }
251
252    fn import_messages<'a>(
253        &'a self,
254        messages: Vec<Message>,
255        mut file: Box<dyn WriteSeek + 'a>,
256        filename: &str,
257        encoding: Encoding,
258        replacement: Option<&'a ReplacementTable>,
259    ) -> Result<()> {
260        let mut texts_filename = std::path::PathBuf::from(filename);
261        texts_filename.set_file_name("TEXT.DAT");
262        let mut texts = Vec::new();
263        let mut reader = self.texts.to_ref();
264        reader.pos = 0x10;
265        while !reader.is_eof() {
266            reader.pos += 4; // Skip index
267            texts.push(reader.read_cstring()?)
268        }
269        let mut texts_file = std::fs::File::create(&texts_filename)
270            .map_err(|e| anyhow::anyhow!("Failed to create TEXT.DAT file: {}", e))?;
271        file.write_all(&self.data.data)?;
272        let mut mes = messages.iter();
273        let mut mess = mes.next();
274        let texts_data_len = self.texts.data.len() as u32;
275        let mut num_offset_map: HashMap<u32, u32> = HashMap::new();
276        for str in &self.strs {
277            let addr = self.data.cpeek_u32_at(str.offset as u64)?;
278            if addr + 4 > texts_data_len {
279                continue;
280            }
281            if str.typ.is_label() {
282                continue; // Ignore labels
283            }
284            let m = match mess {
285                Some(m) => m,
286                None => return Err(anyhow::anyhow!("Not enough messages.")),
287            };
288            let mut text = match str.typ {
289                StringType::Name => match &m.name {
290                    Some(name) => name.clone(),
291                    None => return Err(anyhow::anyhow!("Missing name for message.")),
292                },
293                StringType::Message => {
294                    let m = m.message.clone();
295                    mess = mes.next();
296                    m
297                }
298                StringType::Hover => {
299                    let m = m.message.clone();
300                    mess = mes.next();
301                    m
302                }
303                StringType::Label => continue, // Ignore labels
304            };
305            if let Some(repl) = replacement {
306                for (from, to) in repl.map.iter() {
307                    text = text.replace(from, to);
308                }
309            }
310            text = text.replace("\n", "<br>");
311            let encoded = encode_string(encoding, &text, false)?;
312            let s = std::ffi::CString::new(encoded)?;
313            let num = texts.len() as u32;
314            num_offset_map.insert(num, str.offset);
315            texts.push(s);
316        }
317        if mess.is_some() || mes.next().is_some() {
318            return Err(anyhow::anyhow!("Some messages were not processed."));
319        }
320        texts_file.write_all(b"$TEXT_LIST__")?;
321        texts_file.write_u32(texts.len() as u32)?;
322        let mut nf = MemWriter::new();
323        for (num, text) in texts.into_iter().enumerate() {
324            let num = num as u32;
325            let newaddr = nf.pos as u32 + 0x10;
326            if let Some(offset) = num_offset_map.get(&num) {
327                file.write_u32_at(*offset as u64, newaddr)?;
328            }
329            nf.write_u32(num)?;
330            nf.write_cstring(&text)?;
331        }
332        nf.pos = 0;
333        let mut shift = 4;
334        for _ in 0..(nf.data.len() / 4) {
335            let mut data = nf.cpeek_u32()?;
336            data ^= 0x084DF873 ^ 0xFF987DEE;
337            let mut add = data.to_le_bytes();
338            add[0] = add[0].rotate_right(shift);
339            shift = (shift + 1) % 8;
340            data = u32::from_le_bytes(add);
341            nf.write_u32(data)?;
342        }
343        texts_file.write_all(&nf.data)?;
344        Ok(())
345    }
346
347    fn import_multiple_messages<'a>(
348        &'a self,
349        messages: HashMap<String, Vec<Message>>,
350        mut file: Box<dyn WriteSeek + 'a>,
351        filename: &str,
352        encoding: Encoding,
353        replacement: Option<&'a ReplacementTable>,
354    ) -> Result<()> {
355        let mut texts_filename = std::path::PathBuf::from(filename);
356        texts_filename.set_file_name("TEXT.DAT");
357        let mut texts = Vec::new();
358        let mut reader = self.texts.to_ref();
359        reader.pos = 0x10;
360        while !reader.is_eof() {
361            reader.pos += 4; // Skip index
362            texts.push(reader.read_cstring()?)
363        }
364        let mut texts_file = std::fs::File::create(&texts_filename)
365            .map_err(|e| anyhow::anyhow!("Failed to create TEXT.DAT file: {}", e))?;
366        file.write_all(&self.data.data)?;
367        let hover_messages = messages.get("hover").cloned().unwrap_or_default();
368        let mut hover_iter = hover_messages.iter();
369        let mut hover_mes = hover_iter.next();
370        let mut cur_label: Option<String> = None;
371        let mut cur_messages = messages
372            .get(cur_label.as_ref().map(|s| s.as_str()).unwrap_or("default"))
373            .cloned()
374            .unwrap_or_default();
375        let mut cur_iter = cur_messages.iter();
376        let mut cur_mes = cur_iter.next();
377        let texts_data_len = self.texts.data.len() as u32;
378        let mut num_offset_map: HashMap<u32, u32> = HashMap::new();
379        for str in &self.strs {
380            let addr = self.data.cpeek_u32_at(str.offset as u64)?;
381            if addr + 4 > texts_data_len {
382                continue;
383            }
384            let mut text = match str.typ {
385                StringType::Label => {
386                    if cur_mes.is_some() || cur_iter.next().is_some() {
387                        return Err(anyhow::anyhow!(
388                            "Not all messages were used for label {}.",
389                            cur_label.as_ref().map(|s| s.as_str()).unwrap_or("default")
390                        ));
391                    }
392                    let text = self.texts.cpeek_cstring_at(addr as u64 + 4)?;
393                    let text = decode_to_string(self.encoding, text.as_bytes(), false)?
394                        .replace("<br>", "\n");
395                    cur_messages = messages.get(text.as_str()).cloned().unwrap_or_default();
396                    cur_iter = cur_messages.iter();
397                    cur_mes = cur_iter.next();
398                    cur_label = Some(text);
399                    // We don't need update labels
400                    continue;
401                }
402                StringType::Hover => {
403                    let m = match hover_mes {
404                        Some(m) => m,
405                        None => return Err(anyhow::anyhow!("Not enough hover messages.")),
406                    };
407                    let m = m.message.clone();
408                    hover_mes = hover_iter.next();
409                    m
410                }
411                StringType::Name => {
412                    let m = match cur_mes {
413                        Some(m) => m,
414                        None => {
415                            return Err(anyhow::anyhow!(
416                                "Not enough messages for label {}.",
417                                cur_label.as_ref().map(|s| s.as_str()).unwrap_or("default")
418                            ));
419                        }
420                    };
421                    let name = match &m.name {
422                        Some(name) => name.clone(),
423                        None => return Err(anyhow::anyhow!("Missing name for message.")),
424                    };
425                    name
426                }
427                StringType::Message => {
428                    let m = match cur_mes {
429                        Some(m) => m,
430                        None => {
431                            return Err(anyhow::anyhow!(
432                                "Not enough messages for label {}.",
433                                cur_label.as_ref().map(|s| s.as_str()).unwrap_or("default")
434                            ));
435                        }
436                    };
437                    let m = m.message.clone();
438                    cur_mes = cur_iter.next();
439                    m
440                }
441            };
442            if let Some(repl) = replacement {
443                for (from, to) in repl.map.iter() {
444                    text = text.replace(from, to);
445                }
446            }
447            text = text.replace("\n", "<br>");
448            let encoded = encode_string(encoding, &text, false)?;
449            let s = std::ffi::CString::new(encoded)?;
450            let num = texts.len() as u32;
451            num_offset_map.insert(num, str.offset);
452            texts.push(s);
453        }
454        if cur_mes.is_some() || cur_iter.next().is_some() {
455            return Err(anyhow::anyhow!(
456                "Some messages were not processed for label {}.",
457                cur_label.as_ref().map(|s| s.as_str()).unwrap_or("default")
458            ));
459        }
460        if hover_mes.is_some() || hover_iter.next().is_some() {
461            return Err(anyhow::anyhow!("Some hover messages were not processed."));
462        }
463        texts_file.write_all(b"$TEXT_LIST__")?;
464        texts_file.write_u32(texts.len() as u32)?;
465        let mut nf = MemWriter::new();
466        for (num, text) in texts.into_iter().enumerate() {
467            let num = num as u32;
468            let newaddr = nf.pos as u32 + 0x10;
469            if let Some(offset) = num_offset_map.get(&num) {
470                file.write_u32_at(*offset as u64, newaddr)?;
471            }
472            nf.write_u32(num)?;
473            nf.write_cstring(&text)?;
474        }
475        nf.pos = 0;
476        let mut shift = 4;
477        for _ in 0..(nf.data.len() / 4) {
478            let mut data = nf.cpeek_u32()?;
479            data ^= 0x084DF873 ^ 0xFF987DEE;
480            let mut add = data.to_le_bytes();
481            add[0] = add[0].rotate_right(shift);
482            shift = (shift + 1) % 8;
483            data = u32::from_le_bytes(add);
484            nf.write_u32(data)?;
485        }
486        texts_file.write_all(&nf.data)?;
487        Ok(())
488    }
489
490    fn custom_output_extension<'a>(&'a self) -> &'a str {
491        "txt"
492    }
493
494    fn custom_export(&self, filename: &std::path::Path, _encoding: Encoding) -> Result<()> {
495        let file = std::fs::File::create(filename)
496            .map_err(|e| anyhow::anyhow!("Failed to create file {}: {}", filename.display(), e))?;
497        let mut file = std::io::BufWriter::new(file);
498        Disasm::new(&self.data.data, &self.label_offsets)?.disassemble(Some(&mut file))?;
499        Ok(())
500    }
501}